(($) => {
    if ($.valHooks.div) {
        return;
    }
    const eleHooks = "div,span,ul,ol,li,table,tr,td,th,tbody,thead,tfoot,a,b,caption,p,pre,label,form,font,i".toLowerCase().split(",").map($.trim);

    function Enumerator(array) {
        if (array == null) {
            return;
        }
        let index = 0;
        this.next = function () {
            if (index < array.length) {
                return array[index++];
            }
        };
        this.reset = function () {
            index = 0;
        };
        this.all = function () {
            return array;
        };
        this.get = function (index) {
            return array[index];
        };
    }

    function Copied(value) { Object.assign(this, value); }

    function assign(values) {
        const result = {};
        for (let value of values) {
            for (let key of Object.keys(value)) {
                if (key in result) {
                    if (!$.isArray(result[key])) {
                        result[key] = [result[key]];
                    }
                    result[key] = result[key].concat(value[key]);
                } else {
                    result[key] = value[key];
                }
            }
        }
        return result;
    }

    const handlers = [
        {
            is(jq) { return jq.is(":checkbox,:radio") },
            getValue(jq) { return jq.is(":checked") ? jq.val() : null; },
            setValue(jq, value) { jq.val([].concat(value)); }
        },
        {
            is(jq) { return jq.is("[multiple]") },
            getValue(jq) { return [].concat(jq.val()) },
            setValue(jq, value) { jq.val([].concat(value)); }
        },
        {
            is(jq) { return eleHooks.includes(jq[0].nodeName.toLowerCase()) },
            getValue(jq) { return jq.val(); },
            setValue(jq, value) { jq.val(value); }
        },
        {
            is(jq) { return jq.is(":input,img") || jq.children().length > 0 },
            getValue(jq) { return jq.val(); },
            setValue(jq, value) { jq.val(value); }
        },
        {
            is(jq) { return jq.is("[value]"); },
            getValue(jq) { return jq.attr("value"); },
            setValue(jq, value) { jq.attr("value", value); }
        },
        {
            is(jq) { return true },
            getValue(jq) { return jq.text(); },
            setValue(jq, value) { (jq.is("[name]")) && jq.text(value); }
        },
    ];

    function getValue(jq) {
        for (let handler of handlers) {
            if (handler.is(jq)) {
                return handler.getValue(jq);
            }
        }
    }

    function setValue(jq, value) {
        if (value == null) {
            return;
        }
        const oldValue = getValue(jq);
        for (let handler of handlers) {
            if (handler.is(jq) && !equals(oldValue, value)) {
                handler.setValue(jq, value);
                if (!equals(oldValue, getValue(jq))) {
                    jq.triggerHandler("change");
                }
                return;
            }
        }
    }

    function equals(value1, value2) {
        if (value1 == null || value2 == null) {
            return value1 == value2;
        }
        if ($.isArray(value1) && $.isArray(value2)) {
            if (value1.length !== value2.length) {
                return false;
            }
            value1 = value1.sort();
            value2 = value2.sort();
            for (let i = 0; i < value1.length; i++) {
                if (!equals(value1[i], value2[i])) {
                    return false;
                }
            }
            return true;
        }
        return value1 == value2;
    }

    function getName(jq) {
        const name = $.trim(jq.attr("name"));
        if (!name && jq.is(":input")) {
            return $.trim(jq.attr("id"));
        }
        return name;
    }

    function isPlainObject(obj) {
        try {
            return $.isPlainObject(obj);
        } catch (e) {
            return true;
        }
    }

    const valHook = {
        get(ele) {
            const jq = $(ele);
            const children = jq.children(":not(template)");
            if (children.length === 0) {
                if (jq.is("[name]")) {
                    let value = jq.prop("value");
                    if (value != null) {
                        return value;
                    }
                    value = jq.attr("value");
                    if (value != null) {
                        return value;
                    }
                    return jq.text();
                }
            }
            const values = children.map(function () {
                const jq = $(this);
                if (jq.is(":checkbox,:radio,option") && !jq.is(":checked")) {
                    return null;
                }
                const name = getName(jq);
                if (name) {
                    return { [name]: getValue(jq) };
                }
                const val = jq.val();
                if ($.isPlainObject(val)) {
                    return val;
                }
                return null;
            }).toArray().filter(x => x != null);
            return assign(values);
        },
        set(ele, value) {
            const jq = $(ele);
            const children = jq.children();
            if (children.length === 0) {
                handlers[handlers.length - 1].setValue(jq, value);
                return ele;
            }
            const copied = value instanceof Copied || !isPlainObject(value) || Object.keys(value).length === 0 ? value : new Copied(value);

            return children.each(function () {
                const jq = $(this);
                const name = getName(jq);
                if (!name) {
                    setValue(jq, copied);
                    return this;
                }

                if (copied.hasOwnProperty(name)) {
                    let val = copied[name];
                    if ($.isArray(val)) {
                        val = copied[name] = new Enumerator(val);
                    }
                    if (!(val instanceof Enumerator)) {
                        setValue(jq, val);
                    } else if (jq.is("select[multiple],:checkbox,:radio")) {
                        setValue(jq, val.all());
                    } else {
                        if (jq.is("[reset]")) {
                            val.reset();
                        }
                        setValue(jq, val.next());
                    }
                }
                return this;
            });
        }
    };

    $.extend($.valHooks, assign(eleHooks.map(x => ({ [x]: valHook }))));
    $.extend($.valHooks, {
        img: {
            get(ele) {
                return ele.src;
            },
            set(ele, value) {
                ele.src = value;
            }
        }
    })
})(jQuery);